﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Data.Linq;
using System.Xml;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using uk.ac.soton.ses.DAL;
using System.Diagnostics;
using System.Data.EntityClient;
using System.Security.Principal;

namespace uk.ac.soton.ses
{

    public sealed class ExperimentMetadataContainerDB : ExperimentMetadataContainer, IDisposable
    {
        //Fields
        private Experiment _experimentDB = null;
        //private List<ExperimentDataFileMetadataContainerDB> _experimentDataFilesDB = null;
        private MaterialsMetadataContext _context = null;
        private static string _connectionString = "";

        //Properties
        private MaterialsMetadataContext DatabaseContext { get { return this._context; } }

        private static string ConnectionString
        {
            get
            {
                if (ExperimentMetadataContainerDB._connectionString == "")
                {
                    ExperimentMetadataContainerDB._connectionString = ExperimentMetadataContainerDB.GetEntityConnectionString();
                }
                return ExperimentMetadataContainerDB._connectionString;
            }
        }

        //Constructors

        private ExperimentMetadataContainerDB() : base()
        {
            //Set up data context
            this._context = new MaterialsMetadataContext(ExperimentMetadataContainerDB.ConnectionString);
        }

        public ExperimentMetadataContainerDB(string name, string basePath) : this()
        {
            this.LoadDatabaseDataByName(name, basePath);
            this.CopyDatabaseDataToBusinessObjects();

            //base._experimentDataFiles = this._experimentDataFilesDB.Select(df => new ExperimentDataFileMetadataContainer(new ExperimentDataFileBO(df), this)).ToList();
        }

        public ExperimentMetadataContainerDB(int id) : this()
        {
            this.LoadDatabaseDataByID(id);
            this.CopyDatabaseDataToBusinessObjects();

            //base._experimentDataFiles = this._experimentDataFilesDB.Select(df => new ExperimentDataFileMetadataContainer(new ExperimentDataFileBO(df), this)).ToList();
        }

        private void LoadDatabaseDataByName(string name, string basePath)
        {
            this._experimentDB = this._context.Experiments.SingleOrDefault(exp => exp.Name == name && exp.DefaultBasePath == basePath && exp.IsDeleted == false);
            if (this._experimentDB != null)
            {
                this.LoadExperimentDataFilesFromExperiment(this._experimentDB);
            }
            else
            {
                throw new ExperimentMetadataContainerException(string.Format("Record with name {0}, basePath {1} not found", name, basePath));
            }
        }

        private void LoadDatabaseDataByID(int id)
        {
            this._experimentDB = this._context.GetExperimentByID(id);
            if (this._experimentDB != null)
            {
                this.LoadExperimentDataFilesFromExperiment(this._experimentDB);
            }
            else
            {
                throw new ExperimentMetadataContainerException(string.Format("Record with ID {0} not found", id));
            }
        }

        private void LoadExperimentDataFilesFromExperiment(Experiment experimentDB)
        {
            if (experimentDB != null)
            {
                IEnumerable<ExperimentDataFile> matchingFiles = experimentDB.ExperimentDataFiles.Where(df => df.IsDeleted == false);
                this._experimentDataFiles = new List<ExperimentDataFileMetadataContainer>();
                this._experimentDataFiles.AddRange(matchingFiles.Select(df => new ExperimentDataFileMetadataContainerDB(df, this)));
            }
            else
            {
                throw new ExperimentMetadataContainerException("Problem load experiment data files from the passed experiment");
            }
        }

        private void CopyDatabaseDataToBusinessObjects()
        {
            base._experiment = new ExperimentBO(this._experimentDB);
        }


        public override void SaveChanges()
        {
            base.SaveChanges();

            int numberOfChanges = this._context.SaveChanges();
        }

        override public ExperimentDataFileMetadataContainer AddExperimentDataFile(ExperimentDataFileBO experimentDataFile)
        {
            //TODO: Error checking here for existing records
            ExperimentDataFile newRecord = experimentDataFile.CopyToDB();
            newRecord.FileStoreOptionsExperimentID = HDCConfiguration.Instance.ConfigurationOptions.FileSystemWatcherOptions.ConfigurationContainerID;
            try
            {

                newRecord.User = this._context.GetUserOrCreate(new SecurityIdentifier(newRecord.FileSystemUserID));
            }
            catch (InvalidOperationException)
            {
                //Not bothered if we can't set the User, which will default to the system account
            }

            //Add it to our experiment
            this._experimentDB.ExperimentDataFiles.Add(newRecord);

            //We can only do this if we saved to the database (as we need the database file ID)
            this._context.SaveChanges();


            ExperimentDataFileMetadataContainerDB newFile = new ExperimentDataFileMetadataContainerDB(newRecord, this);
            base._experimentDataFiles.Add(newFile);
            return newFile;
        }

        public void DeleteExperiment()
        {
            this._experimentDB.IsDeleted = true;
            int numberOfFiles = this._experimentDataFiles.Count;
            for (int i = 0; i < numberOfFiles; i++)
            {
                //foreach (ExperimentDataFileMetadataContainerDB fileDB in this._experimentDataFiles)
                //It's always the first one in the list as we delete it every time
                ExperimentDataFileMetadataContainerDB fileDB = (ExperimentDataFileMetadataContainerDB)this._experimentDataFiles[0];
                fileDB.DeleteExperimentDataFile();
                //numberOfFiles--;
                //i--;
            }

            //base._experiment.IsDeleted = true;
            //foreach (ExperimentDataFileBO file in base._experimentDataFiles)
            //    file.IsDeleted = true;
        }

        public bool DeleteExperimentDataFile(int id)
        {
            if (this._experimentDataFiles.Count(df => ((ExperimentDataFileMetadataContainerDB)df).ExperimentDataFileDB.ID == id) == 1)
            {
                ExperimentDataFileMetadataContainer dataFileToDeleteDB = this._experimentDataFiles.Single(df => ((ExperimentDataFileMetadataContainerDB)df).ExperimentDataFileDB.ID == id);
                dataFileToDeleteDB.DeleteExperimentDataFile();

                //ExperimentDataFileBO datafileToDelete = base._experimentDataFiles.Single(df => df.ID == id);
                //datafileToDelete.IsDeleted = true;
                return true;
            }
            else
            {
                return false;
            }
        }

        public override void RenameExperiment(string newName, string newExperimentBasePath)
        {
            base.RenameExperiment(newName, newExperimentBasePath);

            this._experimentDB.Name = newName;
            this._experimentDB.DefaultBasePath = newExperimentBasePath;
            this.SaveChanges();
        }

        public bool RenameExperimentDataFile(int id, string newname)
        {
            if (this._experimentDataFiles.Count(df => ((ExperimentDataFileMetadataContainerDB)df).ExperimentDataFileDB.ID == id) == 1)
            {
                ExperimentDataFileMetadataContainerDB dataFileToRenameDB = this._experimentDataFiles.Single(df => ((ExperimentDataFileMetadataContainerDB)df).ExperimentDataFileDB.ID == id) as ExperimentDataFileMetadataContainerDB;
                if (dataFileToRenameDB != null)
                {
                    dataFileToRenameDB.RenameExperimentDataFile(newname, dataFileToRenameDB.ExperimentDataFileDB.BasePath);
                    this.SaveChanges();
                    return true;
                }
                else
                {
                    return false;
                    //dataFileToRenameDB.Filename = newname;
                }
            }
            else
            {
                return false;
            }
        }

        public ExperimentMetadataContainerXML SaveToXML(bool overwrite)
        {
            if (!Directory.Exists(this._experimentDB.DefaultBasePath))
            {
                throw new DirectoryNotFoundException(string.Format("{0} directory not found", this._experimentDB.DefaultBasePath));
            }
            return ExperimentMetadataContainerXML.Create(this, overwrite);
        }

        /// <summary>
        /// Create a new XML container, storing a previous XML container in the history
        /// </summary>
        /// <param name="existingXML">The existing data to save in the history</param>
        /// <returns></returns>
        public ExperimentMetadataContainerXML SaveToXML(ExperimentMetadataContainerXML existingXML)
        {
            //Create the new file
            ExperimentMetadataContainerXML newXmlMeta = this.SaveToXML(true);

            //Copy the old data entries into the new file starting with the history entries
            foreach(ExperimentHistoryEntry entry in existingXML.History.ExperimentHistoryEntries)
                newXmlMeta.History.ExperimentHistoryEntries.Add(entry);
            foreach(ExperimentDataFileHistory entry in existingXML.History.ExperimentDataFilesHistory)
                newXmlMeta.History.ExperimentDataFilesHistory.Add(entry);

            //Save the old experiment and experimentdatafiles
            newXmlMeta.SaveToHistory(existingXML.Experiment, ExperimentMetadataAction.Updated);
            foreach (ExperimentDataFileMetadataContainer dfMeta in existingXML.ExperimentDataFiles)
            {
                newXmlMeta.SaveToHistory(dfMeta.ExperimentDataFile, ExperimentMetadataAction.Updated);
            }
            newXmlMeta.SaveChanges();
            return newXmlMeta;
        }


        //Static Methods

        public static ExperimentMetadataContainerDB Create(ExperimentMetadataContainerFileSystem fsMetadata)
        {
            using (MaterialsMetadataContext context = new MaterialsMetadataContext(ExperimentMetadataContainerDB.ConnectionString))
            {
                int existingExperimentsCount = context.Experiments.Count(exp => exp.Name == fsMetadata.Experiment.Name && exp.DefaultBasePath == fsMetadata.Experiment.DefaultBasePath && exp.IsDeleted == false);
                if (existingExperimentsCount > 1)
                {
                    throw new ExperimentMetadataContainerException("Something wrong with the database: More than one experiment matches");
                }
                if (existingExperimentsCount == 1)
                {
                    throw new ExperimentMetadataContainerException("Record already exists. Load the data and update it, or explictly call Delete first.");
                }

                //Create a new experiment and list of experiment_data_file, rather than interfere with the objects passed in
                Experiment newExperiment = fsMetadata.Experiment.CopyToDB();

                try
                {
                    //Set the owner
                    newExperiment.User = context.GetUserOrCreate(new SecurityIdentifier(newExperiment.FileSystemUserID));
                }
                catch (InvalidOperationException)
                {
                    //We are not bothered that the user could not be set as the experiment has been saved.
                }

                //Lookup the configuration ID once and use it in loop
                int configurationContainerID = HDCConfiguration.Instance.ConfigurationOptions.FileSystemWatcherOptions.ConfigurationContainerID;

                //Add data files to the experiment
                foreach (ExperimentDataFileMetadataContainer dfcontainer in fsMetadata.ExperimentDataFiles)
                {
                    ExperimentDataFile newdatafile = dfcontainer.ExperimentDataFile.CopyToDB();

                    //Set the file system watcher configuration container
                    newdatafile.FileStoreOptionsExperimentID = configurationContainerID;

                    try
                    {
                        //Set the owner
                        newdatafile.User = context.GetUserOrCreate(new SecurityIdentifier(newdatafile.FileSystemUserID));
                    }
                    catch (InvalidOperationException)
                    {
                        //We are not bothered that the user could not be set as the experiment has been saved.
                    }

                    //Add the datafile to the experiment
                    newExperiment.ExperimentDataFiles.Add(newdatafile);
                }

                //Add the new experiment to the database
                context.Experiments.AddObject(newExperiment);

                //Save the experiment
                context.SaveChanges();

                //context.SetExperimentOwner(new SecurityIdentifier(newExperiment.FileSystemUserID), newExperiment.ID);
                //context.SaveChanges();

                //Return a new metadata class populated from the database
                ExperimentMetadataContainerDB dbMeta = new ExperimentMetadataContainerDB(newExperiment.Name, newExperiment.DefaultBasePath);
                return dbMeta;
            }//end of db context using statement
        }

        public static bool MetadataExists(int id)
        {
            using (MaterialsMetadataContext context = new MaterialsMetadataContext(ExperimentMetadataContainerDB.ConnectionString))
            {
                return context.Experiments.Count(exp => exp.ID == id) == 1;
            }
        }
        public static bool MetadataExists(string name, string defaultBasePath)
        {
            using (MaterialsMetadataContext context = new MaterialsMetadataContext(ExperimentMetadataContainerDB.ConnectionString))
            {
                return context.Experiments.Count(exp => exp.Name == name && exp.DefaultBasePath == defaultBasePath && exp.IsDeleted == false) == 1;
            }
        }

        public static string GetEntityConnectionString()
        {
            HDCConfigurationOptions configOptions = HDCConfiguration.GetConfigurationOptions();
            return configOptions.DatabaseOptions.EntityConnectionString;
        }


        public void Dispose()
        {
            this._context.Dispose();
        }
    }

}
